package se.kth.ik2200.carenet_2012;

/**
 * Created with IntelliJ IDEA.
 * User: Ruslan Mukhammadov
 * Date: November 12, 2012
 * Time: 4:29 PM
 * To change this template use File | Settings | File Templates.
 */

import com.intel.bluetooth.BluetoothConsts;

import javax.bluetooth.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.Vector;

/**
 * Current class is considered to be the main class in the
 * project. Everything in the project has to be bound with
 * this class. First it discovers all available and paired
 * devices, then searches for suitable services for chosen
 * devices,makes connection and then reads data from those
 * devices.
 *
 * Currently this application reads data from two devices:
 * 1. Nonin Onyx II 9560
 * 2. Zephyr BioHarness BT
 *
 * Each of them has proper class to work with in the app.
 * Selection range of these two devices out from the list
 * is based on their hardcoded device names. If there are
 * devices that have names containing 'nonin' or 'harness'
 * words,then the application sees those devices as nonin
 * or zephyr sensor depending on the names they have.
 *
 * Note: In order to read data from the device, it MUST be
 * paired with PC/host.
 */
public class DeviceServer extends Thread {

    private final int SERVICE_NAME = 0x0100;

    private DiscoveryAgent discoveryAgent;

    /**
     * Current variable needed when enquiry and service search
     * is completed.  It is redundant to create separate locks
     * for each of them.
     */
    private final Object lock = new Object();

    /**
     * Current list contains paired and discovered devices
     * within specific bluetooth area range.
     */
    private Vector<RemoteDevice> remoteDevices = new Vector<RemoteDevice>();

    /**
     * Current variable stores the device that has been selected
     * by user to work with. Currently two device models are OK.
     * They are mentioned above.
     */
    private Device selectedDevice;

    /**
     * Current variable stores service of specific device.
     * This service is used then to read data from device.
     * It actually contains a connection URL and it can be
     * called serverURL.
     */
    private String service;

    private String username;

    public static void main(String[] args) {
        DeviceServer server = new DeviceServer();
        server.start();
    }

    /**
     * Starts the thread when it is called from main method.
     */
    public void run() {
        Log.printTitle("Welcome to the Health Care Application!");

        try {
            login();

            if (!LocalDevice.isPowerOn()) {
                Log.printTitle("Bluetooth Radio is not turned ON. Terminating...");
                return;
            }

            LocalDevice localDevice = LocalDevice.getLocalDevice();
            localDevice.setDiscoverable(DiscoveryAgent.GIAC);

            discoveryAgent = localDevice.getDiscoveryAgent();

            reload();
        } catch (BluetoothStateException ex) {
            Log.printError(ex.getMessage());

            ex.printStackTrace();
        } catch (InterruptedException ex) {
            Log.printError(ex.getMessage());

            ex.printStackTrace();
        } catch (IOException ex) {
            Log.printError(ex.getMessage());

            ex.printStackTrace();
        } catch (NoSuchAlgorithmException ex) {
            Log.printError(ex.getMessage());

            ex.printStackTrace();
        } catch (KeyManagementException ex) {
            Log.printError(ex.getMessage());

            ex.printStackTrace();
        }
    }

    /**
     * Currently this application works with two devices:
     * 1. Nonin Onyx II 9560
     * 2. Zephyr BioHarness BT
     *
     * If there are discovered devices other than those
     * two, and the user selected one of them, then the
     * application ignores  that selection by returning
     * false value.
     *
     * @param device - selected remote device to read.
     * @return - returns true if the device is compatible
     *           with application, otherwise returns false.
     * @throws java.io.IOException
     */
    private boolean checkDeviceCompatibility(RemoteDevice device) throws IOException {
        String name = Device.extractDeviceName(device).toLowerCase();
        if (name.contains("bh") || name.contains("harness")) {
            selectedDevice = new ZephyrBioHarness();
        } else if (name.contains("nonin")) {
            selectedDevice = new NoninOnyx();
        }

        if (selectedDevice != null) {
            selectedDevice.setInstance(device);
            return true;
        }

        String errorMessage = "Sorry, selected device is not compatible. Currently only two device models are compatible:";

        Log.printError(errorMessage);
        Log.printOrdinaryLog(" 1. " + NoninOnyx.DEVICE_MODEL);
        Log.printOrdinaryLog(" 2. " + ZephyrBioHarness.DEVICE_MODEL + "\n");

        return false;
    }

    /**
     * Discovers devices in a bluetooth area range of the host.
     *
     * @throws InterruptedException
     */
    private void discoverDevices() throws BluetoothStateException, InterruptedException {
        // Each time before new search, previous list has to be cleared.
        remoteDevices.clear();

        DiscoveryListener discoveryListener = new DiscoveryListener() {
            @Override
            public void deviceDiscovered(RemoteDevice bluetoothDevice, DeviceClass cod) {
                // Adding discovered devices to the list.
                remoteDevices.add(bluetoothDevice);

                try {
                    printDeviceInfo(remoteDevices.size(), bluetoothDevice);
                } catch (IOException ex) {
                    Log.printError(ex.getMessage());
                    ex.printStackTrace();
                }
            }

            @Override
            public void inquiryCompleted(int discType) {
                Log.printOrdinaryLog("\n 0. Start device search again");
                Log.printOrdinaryLog("-1. Exit\n");

                String log = "\nDevice enquiry is completed. " + remoteDevices.size() + " device(s) were found.\n";
                Log.printOrdinaryLog(log);

                synchronized (lock) {
                    lock.notifyAll();
                }
            }

            @Override
            public void servicesDiscovered(int transactionID, ServiceRecord[] serviceRecords) {
            }

            @Override
            public void serviceSearchCompleted(int transactionID, int responseCode) {
            }
        };

        synchronized (lock) {
            boolean started = discoveryAgent.startInquiry(DiscoveryAgent.GIAC, discoveryListener);

            if (started) {
                Log.printOrdinaryLog("Searching Devices...\n");

                lock.wait();
            }
        }
    }

    /**
     * Primary logic here is that each user is registered in OpenMRS and has username and
     * password.Only by providing existing username and password in the OpenMRS,users can
     * upload/stream their sensor data into the OpenMRS database.The this specific user's
     * sensor data will be shown in Carenet Web Page.
     */
    private void login() throws IOException, NoSuchAlgorithmException, KeyManagementException {
        BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
        Log.printWithoutLine("Username: ");
        username = input.readLine();

        input = new BufferedReader(new InputStreamReader(System.in));
        Log.printWithoutLine("Password: ");
        String password = input.readLine();


        if (DataTransmissionHandler.login(username, password)) {
            Log.printOrdinaryLog("You have successfully Logged In!\n");
        } else {
            Log.printWarning("Sorry, such username and/or password do not exist in the OpenMRS. Exiting...");
            System.exit(0);
        }
    }

    /**
     * Manages all discovered devices. The user can choose one of the option
     * from provided device found the list.If everything is correct, it will
     * continue to search a service for chosen device and start reading it.
     *
     * @param selectedOption - selected number out of 'remoteDevices.size()'
     * @throws java.io.IOException
     * @throws InterruptedException
     */
    private void manageDiscoveredDevices(String selectedOption)
            throws InterruptedException, IOException, NoSuchAlgorithmException, KeyManagementException {
        int options = remoteDevices.size();
        String errorMessage;

        if (options > 1) {
            errorMessage = "Please enter a number between -1 and " + options;
        } else {
            errorMessage = "Please type 1 to continue, 0 to start search again.";
        }

        try {
            int option = Integer.parseInt(selectedOption);

            if (option < -1 || option > remoteDevices.size()) {
                Log.printError(errorMessage);
                restart();
                return;
            }

            if (option == -1) {
                System.exit(0);
            }

            // Start all over again.
            if (option == 0) {
                reload();
            }

            RemoteDevice chosenDevice = remoteDevices.get(option - 1);// Because Vector<> starts from '0';
            if (checkDeviceCompatibility(chosenDevice)) {
                searchService();

                if (service == null) {
                    restart();
                } else {
                    selectedDevice.readDevice(service, username);
                }
            } else {
                restart();
            }
        } catch (NumberFormatException ex) {
            Log.printError(errorMessage);
            restart();
        }
    }

    /**
     * Starts all device discovery and service searches from the scratch.
     *
     * @throws InterruptedException
     * @throws java.io.IOException
     */
    private void reload() throws InterruptedException, IOException, NoSuchAlgorithmException, KeyManagementException {
        // First, devices have to be discovered
        discoverDevices();

        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

        String line = "Please Enter a number between -1 and " + remoteDevices.size() + ": ";
        Log.printWithoutLine(line);

        String selectedOption = in.readLine();
        manageDiscoveredDevices(selectedOption);
    }

    /**
     * Restarts everything from the previous session.So,
     * nothing will be started from the scratch, all the
     * previously  found list of  remote devices will be
     * provided instead.
     *
     * @throws java.io.IOException
     * @throws InterruptedException
     */
    private void restart() throws InterruptedException, IOException, NoSuchAlgorithmException, KeyManagementException {
        Log.printOrdinaryLog("\nRestarting the session...\n");
        Log.printOrdinaryLog("Previously discovered devices: ");

        for (int i = 0; i < remoteDevices.size(); i++) {
            RemoteDevice remoteDevice = remoteDevices.get(i);
            printDeviceInfo(i + 1, remoteDevice);
        }
        Log.printOrdinaryLog("\n 0. Start device search again");
        Log.printOrdinaryLog("-1. Exit\n");

        Log.printWithoutLine("Please select one option: ");
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        String value = in.readLine();

        manageDiscoveredDevices(value);
    }

    private void searchService() throws InterruptedException, IOException {
        DiscoveryListener serviceSearchListener = new DiscoveryListener() {
            @Override
            public void deviceDiscovered(RemoteDevice bluetoothDevice, DeviceClass cod) {
            }

            @Override
            public void inquiryCompleted(int discType) {
            }

            @Override
            public void servicesDiscovered(int transactionID, ServiceRecord[] serviceRecords) {
                for (ServiceRecord serviceRecord : serviceRecords) {
                    int securityLevel = ServiceRecord.NOAUTHENTICATE_NOENCRYPT;
                    String url = serviceRecord.getConnectionURL(securityLevel, false);

                    if (url == null) {
                        continue;
                    }

                    service = url;

                    DataElement serviceName = serviceRecord.getAttributeValue(SERVICE_NAME);
                    if (serviceName != null) {
                        String log = "Service Name: " + serviceName.getValue() + "  Connection URL: " + url;
                        Log.printOrdinaryLog(log);
                    } else {
                        Log.printOrdinaryLog("Service found, Connection URL: " + url);
                    }
                }
            }

            @Override
            public void serviceSearchCompleted(int transactionID, int responseCode) {
                if (service == null) {
                    Log.printOrdinaryLog("No service found.");
                } else {
                    Log.printOrdinaryLog("1 service is found for the device.\n");
                }

                synchronized(lock){
                    lock.notifyAll();
                }
            }
        };

        UUID serviceUUID = BluetoothConsts.RFCOMM_PROTOCOL_UUID;
        UUID[] uuidSet = new UUID[] { serviceUUID };
        int[] attrIDs =  new int[] {
            SERVICE_NAME
        };

        synchronized(lock) {
            Log.printOrdinaryLog("\nSearching a service for " + selectedDevice.getDeviceName() + " device...");

            discoveryAgent.searchServices(attrIDs, uuidSet, selectedDevice.getInstance(), serviceSearchListener);
            lock.wait();
        }
    }

    private void printDeviceInfo(int index, RemoteDevice remoteDevice) throws IOException{
        String deviceName = Device.extractDeviceName(remoteDevice);
        String address = remoteDevice.getBluetoothAddress();

        Log.printOrdinaryLog(" " + index + ". Device Name: " + deviceName + ";  Device Address: " + address);
    }
}
